using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;

namespace Microsoft.Cci.Pdb
{
  internal class PdbFile
  {
    /// <summary>
    /// GUID of the Basic source language.
    /// </summary>
    private static readonly Guid BasicLanguageGuid = new Guid(974311608, -15764, 4560, 180, 66, 0, 160, 36, 74, 29, 210);

    private PdbFile()   // This class can't be instantiated.
    {
    }

    static void LoadInjectedSourceInformation(BitAccess bits, out Guid doctype, out Guid language, out Guid vendor, out Guid checksumAlgo, out byte[] checksum)
    {
      checksum = null;

      bits.ReadGuid(out language);
      bits.ReadGuid(out vendor);
      bits.ReadGuid(out doctype);
      bits.ReadGuid(out checksumAlgo);
      bits.ReadInt32(out int checksumSize);
      bits.ReadInt32(out int injectedSourceSize);

      if (checksumSize > 0)
      {
        checksum = new byte[checksumSize];
        bits.ReadBytes(checksum);
      }
    }

    static Dictionary<string, int> LoadNameIndex(BitAccess bits, out int age, out Guid guid)
    {
      Dictionary<string, int> result = new Dictionary<string, int>();
      bits.ReadInt32(out int ver);    //  0..3  Version
      bits.ReadInt32(out int sig);    //  4..7  Signature
      bits.ReadInt32(out age);    //  8..11 Age
      bits.ReadGuid(out guid);       // 12..27 GUID

      //if (ver != 20000404) {
      //  throw new PdbDebugException("Unsupported PDB Stream version {0}", ver);
      //}

      // Read string buffer.
      bits.ReadInt32(out int buf);    // 28..31 Bytes of Strings

      int beg = bits.Position;
      int nxt = bits.Position + buf;

      bits.Position = nxt;

      // Read map index.
      // n+0..3 hash size.
      // n+4..7 maximum ni.

      bits.ReadInt32(out int cnt);
      bits.ReadInt32(out int max);

      BitSet present = new BitSet(bits);
      BitSet deleted = new BitSet(bits);
      if (!deleted.IsEmpty)
      {
        throw new PdbDebugException("Unsupported PDB deleted bitset is not empty.");
      }

      int j = 0;
      for (int i = 0; i < max; i++)
      {
        if (present.IsSet(i))
        {
          bits.ReadInt32(out int ns);
          bits.ReadInt32(out int ni);

          int saved = bits.Position;
          bits.Position = beg + ns;
          bits.ReadCString(out string name);
          bits.Position = saved;

          result.Add(name.ToUpperInvariant(), ni);
          j++;
        }
      }
      if (j != cnt)
      {
        throw new PdbDebugException("Count mismatch. ({0} != {1})", j, cnt);
      }
      return result;
    }

    static IntHashTable LoadNameStream(BitAccess bits)
    {
      IntHashTable ht = new IntHashTable();

      bits.ReadUInt32(out uint sig);   //  0..3  Signature
      bits.ReadInt32(out int ver);    //  4..7  Version

      // Read (or skip) string buffer.
      bits.ReadInt32(out int buf);    //  8..11 Bytes of Strings

      if (sig != 0xeffeeffe || ver != 1)
      {
        throw new PdbDebugException("Unsupported Name Stream version. " +
                                            "(sig={0:x8}, ver={1})",
                                    sig, ver);
      }
      int beg = bits.Position;
      int nxt = bits.Position + buf;
      bits.Position = nxt;

      // Read hash table.
      bits.ReadInt32(out int siz);    // n+0..3 Number of hash buckets.
      nxt = bits.Position;

      for (int i = 0; i < siz; i++)
      {

        bits.ReadInt32(out int ni);

        if (ni != 0)
        {
          int saved = bits.Position;
          bits.Position = beg + ni;
          bits.ReadCString(out string name);
          bits.Position = saved;

          ht.Add(ni, name);
        }
      }
      bits.Position = nxt;

      return ht;
    }

    private static PdbFunction match = new PdbFunction();

    private static int FindFunction(PdbFunction[] funcs, ushort sec, uint off)
    {
      match.segment = sec;
      match.address = off;

      return Array.BinarySearch(funcs, match, PdbFunction.byAddress);
    }

    static void LoadManagedLines(PdbFunction[] funcs,
                                 IntHashTable names,
                                 BitAccess bits,
                                 MsfDirectory dir,
                                 Dictionary<string, int> nameIndex,
                                 PdbReader reader,
                                 uint limit,
                                 Dictionary<string, PdbSource> sourceCache)
    {
      Array.Sort(funcs, PdbFunction.byAddressAndToken);

      int begin = bits.Position;
      IntHashTable checks = ReadSourceFileInfo(bits, limit, names, dir, nameIndex, reader, sourceCache);

      // Read the lines next.
      bits.Position = begin;
      while (bits.Position < limit)
      {
        bits.ReadInt32(out int sig);
        bits.ReadInt32(out int siz);
        int endSym = bits.Position + siz;

        switch ((DEBUG_S_SUBSECTION)sig)
        {
          case DEBUG_S_SUBSECTION.LINES:
            {
              CV_LineSection sec;

              bits.ReadUInt32(out sec.off);
              bits.ReadUInt16(out sec.sec);
              bits.ReadUInt16(out sec.flags);
              bits.ReadUInt32(out sec.cod);
              int funcIndex = FindFunction(funcs, sec.sec, sec.off);
              if (funcIndex < 0) break;
              PdbFunction func = funcs[funcIndex];
              if (func.lines == null)
              {
                while (funcIndex > 0)
                {
                  PdbFunction f = funcs[funcIndex - 1];
                  if (f.lines != null || f.segment != sec.sec || f.address != sec.off) break;
                  func = f;
                  funcIndex--;
                }
              }
              else
              {
                while (funcIndex < funcs.Length - 1 && func.lines != null)
                {
                  PdbFunction f = funcs[funcIndex + 1];
                  if (f.segment != sec.sec || f.address != sec.off) break;
                  func = f;
                  funcIndex++;
                }
              }
              if (func.lines != null) break;

              // Count the line blocks.
              int begSym = bits.Position;
              int blocks = 0;
              while (bits.Position < endSym)
              {
                CV_SourceFile file;
                bits.ReadUInt32(out file.index);
                bits.ReadUInt32(out file.count);
                bits.ReadUInt32(out file.linsiz);   // Size of payload.
                int linsiz = (int)file.count * (8 + ((sec.flags & 1) != 0 ? 4 : 0));
                bits.Position += linsiz;
                blocks++;
              }

              func.lines = new PdbLines[blocks];
              int block = 0;

              bits.Position = begSym;
              while (bits.Position < endSym)
              {
                CV_SourceFile file;
                bits.ReadUInt32(out file.index);
                bits.ReadUInt32(out file.count);
                bits.ReadUInt32(out file.linsiz);   // Size of payload.

                PdbSource src = (PdbSource)checks[(int)file.index];
                if (src.language.Equals(BasicLanguageGuid))
                {
                  func.AdjustVisualBasicScopes();
                }

                PdbLines tmp = new PdbLines(src, file.count);
                func.lines[block++] = tmp;
                PdbLine[] lines = tmp.lines;

                int plin = bits.Position;
                int pcol = bits.Position + 8 * (int)file.count;

                for (int i = 0; i < file.count; i++)
                {
                  CV_Line line;
                  CV_Column column = new CV_Column();

                  bits.Position = plin + 8 * i;
                  bits.ReadUInt32(out line.offset);
                  bits.ReadUInt32(out line.flags);

                  uint lineBegin = line.flags & (uint)CV_Line_Flags.linenumStart;
                  uint delta = (line.flags & (uint)CV_Line_Flags.deltaLineEnd) >> 24;
                  //bool statement = ((line.flags & (uint)CV_Line_Flags.fStatement) == 0);
                  if ((sec.flags & 1) != 0)
                  {
                    bits.Position = pcol + 4 * i;
                    bits.ReadUInt16(out column.offColumnStart);
                    bits.ReadUInt16(out column.offColumnEnd);
                  }

                  lines[i] = new PdbLine(line.offset,
                                         lineBegin,
                                         column.offColumnStart,
                                         lineBegin + delta,
                                         column.offColumnEnd);
                }
              }
              break;
            }
        }
        bits.Position = endSym;
      }
    }

    static void LoadFuncsFromDbiModule(BitAccess bits,
                                       DbiModuleInfo info,
                                       IntHashTable names,
                                       List<PdbFunction> funcList,
                                       bool readStrings,
                                       MsfDirectory dir,
                                       Dictionary<string, int> nameIndex,
                                       PdbReader reader,
                                       Dictionary<string, PdbSource> sourceCache)
    {
      PdbFunction[] funcs = null;

      bits.Position = 0;
      bits.ReadInt32(out int sig);
      if (sig != 4)
      {
        throw new PdbDebugException("Invalid signature. (sig={0})", sig);
      }

      bits.Position = 4;
      // Console.WriteLine("{0}:", info.moduleName);
      funcs = PdbFunction.LoadManagedFunctions(/*info.moduleName,*/
                                               bits, (uint)info.cbSyms,
                                               readStrings);
      if (funcs != null)
      {
        bits.Position = info.cbSyms + info.cbOldLines;
        LoadManagedLines(funcs, names, bits, dir, nameIndex, reader,
                         (uint)(info.cbSyms + info.cbOldLines + info.cbLines),
                         sourceCache);

        for (int i = 0; i < funcs.Length; i++)
        {
          funcList.Add(funcs[i]);
        }
      }
    }

    static void LoadDbiStream(BitAccess bits,
                              out DbiModuleInfo[] modules,
                              out DbiDbgHdr header,
                              bool readStrings)
    {
      DbiHeader dh = new DbiHeader(bits);
      header = new DbiDbgHdr();

      //if (dh.sig != -1 || dh.ver != 19990903) {
      //  throw new PdbException("Unsupported DBI Stream version, sig={0}, ver={1}",
      //                         dh.sig, dh.ver);
      //}

      // Read gpmod section.
      List<DbiModuleInfo> modList = new List<DbiModuleInfo>();
      int end = bits.Position + dh.gpmodiSize;
      while (bits.Position < end)
      {
        DbiModuleInfo mod = new DbiModuleInfo(bits, readStrings);
        modList.Add(mod);
      }
      if (bits.Position != end)
      {
        throw new PdbDebugException("Error reading DBI stream, pos={0} != {1}",
                                    bits.Position, end);
      }

      if (modList.Count > 0)
      {
        modules = modList.ToArray();
      }
      else
      {
        modules = null;
      }

      // Skip the Section Contribution substream.
      bits.Position += dh.secconSize;

      // Skip the Section Map substream.
      bits.Position += dh.secmapSize;

      // Skip the File Info substream.
      bits.Position += dh.filinfSize;

      // Skip the TSM substream.
      bits.Position += dh.tsmapSize;

      // Skip the EC substream.
      bits.Position += dh.ecinfoSize;

      // Read the optional header.
      end = bits.Position + dh.dbghdrSize;
      if (dh.dbghdrSize > 0)
      {
        header = new DbiDbgHdr(bits);
      }
      bits.Position = end;
    }

    internal static PdbInfo LoadFunctions(Stream read)
    {
      PdbInfo pdbInfo = new PdbInfo();

      pdbInfo.TokenToSourceMapping = new Dictionary<uint, PdbTokenLine>();
      BitAccess bits = new BitAccess(64 * 1024);
      PdbFileHeader head = new PdbFileHeader(read, bits);
      PdbReader reader = new PdbReader(read, head.pageSize);
      MsfDirectory dir = new MsfDirectory(reader, head, bits);
      Dictionary<string, PdbSource> sourceCache = new Dictionary<string, PdbSource>();

      dir.streams[1].Read(reader, bits);
      Dictionary<string, int> nameIndex = LoadNameIndex(bits, out pdbInfo.Age, out pdbInfo.Guid);
      if (!nameIndex.TryGetValue("/NAMES", out int nameStream))
      {
        throw new PdbException("Could not find the '/NAMES' stream: the PDB file may be a public symbol file instead of a private symbol file");
      }
      dir.streams[nameStream].Read(reader, bits);
      IntHashTable names = LoadNameStream(bits);

      if (!nameIndex.TryGetValue("SRCSRV", out int srcsrvStream))
        pdbInfo.SourceServerData = string.Empty;
      else
      {
        DataStream dataStream = dir.streams[srcsrvStream];
        byte[] bytes = new byte[dataStream.contentSize];
        dataStream.Read(reader, bits);
        pdbInfo.SourceServerData = bits.ReadBString(bytes.Length);
      }

      if (nameIndex.TryGetValue("SOURCELINK", out int sourceLinkStream))
      {
        DataStream dataStream = dir.streams[sourceLinkStream];
        pdbInfo.SourceLinkData = new byte[dataStream.contentSize];
        dataStream.Read(reader, bits);
        bits.ReadBytes(pdbInfo.SourceLinkData);
      }

      dir.streams[3].Read(reader, bits);
      LoadDbiStream(bits, out DbiModuleInfo[] modules, out DbiDbgHdr header, true);

      List<PdbFunction> funcList = new List<PdbFunction>();

      if (modules != null)
      {
        for (int m = 0; m < modules.Length; m++)
        {
          DbiModuleInfo module = modules[m];
          if (module.stream > 0)
          {
            dir.streams[module.stream].Read(reader, bits);
            if (module.moduleName == "TokenSourceLineInfo")
            {
              LoadTokenToSourceInfo(bits, module, names, dir, nameIndex, reader, pdbInfo.TokenToSourceMapping, sourceCache);
              continue;
            }
            LoadFuncsFromDbiModule(bits, module, names, funcList, true, dir, nameIndex, reader, sourceCache);
          }
        }
      }

      PdbFunction[] funcs = funcList.ToArray();

      // After reading the functions, apply the token remapping table if it exists.
      if (header.snTokenRidMap != 0 && header.snTokenRidMap != 0xffff)
      {
        dir.streams[header.snTokenRidMap].Read(reader, bits);
        uint[] ridMap = new uint[dir.streams[header.snTokenRidMap].Length / 4];
        bits.ReadUInt32(ridMap);

        foreach (PdbFunction func in funcs)
        {
          func.token = 0x06000000 | ridMap[func.token & 0xffffff];
        }
      }

      //
      Array.Sort(funcs, PdbFunction.byAddressAndToken);
      //Array.Sort(funcs, PdbFunction.byToken);
      pdbInfo.Functions = funcs;
      return pdbInfo;
    }

    private static void LoadTokenToSourceInfo(BitAccess bits, DbiModuleInfo module, IntHashTable names, MsfDirectory dir,
      Dictionary<string, int> nameIndex, PdbReader reader, Dictionary<uint, PdbTokenLine> tokenToSourceMapping, Dictionary<string, PdbSource> sourceCache)
    {
      bits.Position = 0;
      bits.ReadInt32(out int sig);
      if (sig != 4)
      {
        throw new PdbDebugException("Invalid signature. (sig={0})", sig);
      }

      bits.Position = 4;

      while (bits.Position < module.cbSyms)
      {

        bits.ReadUInt16(out ushort siz);
        int star = bits.Position;
        int stop = bits.Position + siz;
        bits.Position = star;
        bits.ReadUInt16(out ushort rec);

        switch ((SYM)rec)
        {
          case SYM.S_OEM:
            OemSymbol oem;

            bits.ReadGuid(out oem.idOem);
            bits.ReadUInt32(out oem.typind);
            // internal byte[]   rgl;        // user data, force 4-byte alignment

            if (oem.idOem == PdbFunction.msilMetaData)
            {
              string name = bits.ReadString();
              if (name == "TSLI")
              {
                bits.ReadUInt32(out uint token);
                bits.ReadUInt32(out uint file_id);
                bits.ReadUInt32(out uint line);
                bits.ReadUInt32(out uint column);
                bits.ReadUInt32(out uint endLine);
                bits.ReadUInt32(out uint endColumn);
                if (!tokenToSourceMapping.TryGetValue(token, out PdbTokenLine tokenLine))
                  tokenToSourceMapping.Add(token, new PdbTokenLine(token, file_id, line, column, endLine, endColumn));
                else
                {
                  while (tokenLine.nextLine != null) tokenLine = tokenLine.nextLine;
                  tokenLine.nextLine = new PdbTokenLine(token, file_id, line, column, endLine, endColumn);
                }
              }
              bits.Position = stop;
              break;
            }
            else
            {
              throw new PdbDebugException("OEM section: guid={0} ti={1}",
                                          oem.idOem, oem.typind);
              // bits.Position = stop;
            }

          case SYM.S_END:
            bits.Position = stop;
            break;

          default:
            //Console.WriteLine("{0,6}: {1:x2} {2}",
            //                  bits.Position, rec, (SYM)rec);
            bits.Position = stop;
            break;
        }
      }

      bits.Position = module.cbSyms + module.cbOldLines;
      int limit = module.cbSyms + module.cbOldLines + module.cbLines;
      IntHashTable sourceFiles = ReadSourceFileInfo(bits, (uint)limit, names, dir, nameIndex, reader, sourceCache);
      foreach (PdbTokenLine tokenLine in tokenToSourceMapping.Values)
      {
        tokenLine.sourceFile = (PdbSource)sourceFiles[(int)tokenLine.file_id];
      }

    }

    public static readonly Guid SymDocumentType_Text = new Guid(1518771467, 26129, 4563, 189, 42, 0, 0, 248, 8, 73, 189);

    private static IntHashTable ReadSourceFileInfo(BitAccess bits, uint limit, IntHashTable names, MsfDirectory dir,
      Dictionary<string, int> nameIndex, PdbReader reader, Dictionary<string, PdbSource> sourceCache)
    {
      IntHashTable checks = new IntHashTable();

      int begin = bits.Position;
      while (bits.Position < limit)
      {
        bits.ReadInt32(out int sig);
        bits.ReadInt32(out int siz);
        int place = bits.Position;
        int endSym = bits.Position + siz;

        switch ((DEBUG_S_SUBSECTION)sig)
        {
          case DEBUG_S_SUBSECTION.FILECHKSMS:
            while (bits.Position < endSym)
            {
              CV_FileCheckSum chk;

              int ni = bits.Position - place;
              bits.ReadUInt32(out chk.name);
              bits.ReadUInt8(out chk.len);
              bits.ReadUInt8(out chk.type);

              string name = (string)names[(int)chk.name];

              if (!sourceCache.TryGetValue(name, out PdbSource src))
              {
                Guid doctypeGuid = SymDocumentType_Text;
                Guid languageGuid = Guid.Empty;
                Guid vendorGuid = Guid.Empty;
                Guid checksumAlgoGuid = Guid.Empty;
                byte[] checksum = null;
                if (nameIndex.TryGetValue("/SRC/FILES/" + name.ToUpperInvariant(), out int guidStream))
                {
                  BitAccess guidBits = new BitAccess(0x100);
                  dir.streams[guidStream].Read(reader, guidBits);
                  LoadInjectedSourceInformation(guidBits, out doctypeGuid, out languageGuid, out vendorGuid, out checksumAlgoGuid, out checksum);
                }

                src = new PdbSource(name, doctypeGuid, languageGuid, vendorGuid, checksumAlgoGuid, checksum);
                sourceCache.Add(name, src);
              }
              checks.Add(ni, src);
              bits.Position += chk.len;
              bits.Align(4);
            }
            bits.Position = endSym;
            break;

          default:
            bits.Position = endSym;
            break;
        }
      }
      return checks;
    }
  }
}
